/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once
#include "pch.h"
#include <journal/views/list_page_layout.h>

namespace journal { namespace view {

list_page_layout::list_page_layout(
    const b::float32 scale,
    const bool stacked,
    const std::shared_ptr<view::inotebook>& notebook
) : 
    _scale(scale),
    _stacked(stacked),
    _notebook(notebook)
{
    // calculate focus
    b::float32 windowWorldWidth = constants::shellService()->windowSize().x / constants::shellService()->logicalDpi();
    b::float32 aspectRatioPixel = constants::shellService()->windowSize().x / constants::shellService()->windowSize().y;
    _focus.x = windowWorldWidth / _scale;
    _focus.y = _focus.x / aspectRatioPixel;
}

void list_page_layout::setReference(
    const size_t referencePageNumber, 
    const m::point3d referenceWorld
    )
{
    // calculate the world origin (at the center of page zero)
    _originWorld = m::point3d(0.f, 0.f, 0.f);
    m::point3d pagePosition = this->calculatePagePosition(referencePageNumber);
    _originWorld.x = (referenceWorld.x - pagePosition.x);
    _originWorld.y = (referenceWorld.y - pagePosition.y);
    _originWorld.z = (referenceWorld.z - pagePosition.z);
}

m::point3d list_page_layout::calculatePagePosition(const size_t pageNumber)
{
    m::point3d normal;
    this->pageNumberToNormal(pageNumber, &normal);

    m::point3d world;
    this->normalToWorld(normal, &world);

    world.x += this->getSlotWidth() / 2.f;
    world.y -= constants::general::pageWorldDimensions().y / 2.f;

    return world;
}

m::point2d list_page_layout::calculateCameraOffsetXY(const size_t pageNumber)
{
    m::point3d pagePosition = this->calculatePagePosition(pageNumber);
    m::point2d offsetXY(pagePosition.x, pagePosition.y);
    this->constrainOffsetXY(offsetXY);
    return offsetXY;
}

void list_page_layout::constrainOffsetXY(m::point2d& offsetXY)
{
    m::point3d position = this->calculatePagePosition(0);

    if (_focus.y >= constants::general::pageWorldDimensions().y)
    {
        offsetXY.y = position.y; // if enough room, fix the offset at the center of the page
    }
    else
    {
        if (offsetXY.y + _focus.y / 2.f > position.y + constants::general::pageWorldDimensions().y / 2)
        {
            offsetXY.y = position.y + constants::general::pageWorldDimensions().y / 2 - _focus.y / 2.f;
        }
        else if (offsetXY.y - _focus.y / 2.f < position.y - constants::general::pageWorldDimensions().y / 2)
        {
            offsetXY. y = position.y - constants::general::pageWorldDimensions().y / 2 + _focus.y / 2.f;
        }
    }

    if (_focus.x >= constants::general::pageWorldDimensions().x)
    {
        b::float32 x0 = position.x;
        if (offsetXY.x < x0)
        {
            offsetXY.x = x0;
        }
        else 
        {
            b::float32 xN = this->calculatePagePosition(_notebook.lock()->model()->pageCount() - 1).x;
            if (offsetXY.x > xN)
            {
                offsetXY.x = xN;
            }
        }
    }
    else
    {
        if (offsetXY.x + _focus.x / 2.f > position.x + constants::general::pageWorldDimensions().x / 2)
        {
            offsetXY.x = position.x + constants::general::pageWorldDimensions().x / 2 - _focus.x / 2.f;
        }
        else if (offsetXY.x - _focus.x / 2.f < position.x - constants::general::pageWorldDimensions().x / 2)
        {
            offsetXY.x = position.x - constants::general::pageWorldDimensions().x / 2 + _focus.x / 2.f;
        }
    }
}

void list_page_layout::constrainScale(b::float32& scale)
{
    scale = max(scale, 0.01f);
}

void list_page_layout::calculatePageSet(
    const m::vector2d& offsetXY,
    const m::vector2d& focus,
    const page_set& currentPageSet,
    size_t currentPage,
    page_set* pageSet, 
    size_t* pageNumber
    )
{
    if (_stacked)
    {
        pageSet->minPage = currentPage;
        pageSet->maxPage = currentPage;
        *pageNumber = currentPage;

        return;
    }

    m::rectangle cameraBoundsNormal;
    this->calculateCameraBoundsNormal(offsetXY, focus, &cameraBoundsNormal);

    m::rectangle boundsNormal;
    this->calculateBoundsNormal(&boundsNormal);

    pageSet->minPage = NO_PAGE;
    pageSet->maxPage = NO_PAGE;
    pageSet->changeNumber = currentPageSet.changeNumber + 1;

    if (boundsNormal.intersects(cameraBoundsNormal))
    {
        this->normalToPageNumber(
            m::point3d(max(cameraBoundsNormal.left, 0.f), max(cameraBoundsNormal.top, 0.f), 0.f),
            &pageSet->minPage
            );

        this->normalToPageNumber(
            m::point3d(max(cameraBoundsNormal.right, 0.f), max(cameraBoundsNormal.bottom, 0.f), 0.f),
            &pageSet->maxPage
            );
    }

    m::point3d cameraWorld(
        offsetXY.x,
        offsetXY.y,
        0.f
        );

    m::point3d cameraNormal;
    this->worldToNormal(cameraWorld, &cameraNormal);

    // calculate new page number at the center of the camera view
    this->normalToPageNumber(cameraNormal, pageNumber);
}

void list_page_layout::normalToWorld(const m::point3d& pointNormal, m::point3d* pointWorld)
{
    pointWorld->x = (pointNormal.x * this->getSlotWidth()) - (this->getSlotWidth() / 2.f);
    pointWorld->y = -(pointNormal.y * constants::general::pageWorldDimensions().y) + (constants::general::pageWorldDimensions().y / 2.f);
    pointWorld->z = pointNormal.z;

    pointWorld->x += _originWorld.x;
    pointWorld->y += _originWorld.y;
    pointWorld->z += _originWorld.z;
}

void list_page_layout::worldToNormal(const m::point3d& pointWorld, m::point3d* pointNormal)
{
    m::point3d world = pointWorld;
    world.x = pointWorld.x - _originWorld.x;
    world.y = pointWorld.y - _originWorld.y;
    world.z = pointWorld.z - _originWorld.z;

    pointNormal->x = (world.x + (this->getSlotWidth() / 2.f)) / this->getSlotWidth();
    pointNormal->y = (world.y - (constants::general::pageWorldDimensions().y / 2.f)) / constants::general::pageWorldDimensions().y;
    pointNormal->y *= -1.f;
    pointNormal->z = 0;
}

void list_page_layout::pageNumberToNormal(const size_t pageNumber, m::point3d* pointNormal)
{
    pointNormal->x = _stacked ? 0 : static_cast<b::float32>(pageNumber);
    pointNormal->y = 0;
    pointNormal->z = 0;
}

void list_page_layout::normalToPageNumber(const m::point3d& pointNormal, size_t* pageNumber)
{
    // NOTE: this code can be improved to calculate the page number on negative offsets too
    // but since this is something that should never really happen as of now I am not bothering
    if (pointNormal.y < 0) *pageNumber = NO_PAGE;
    else if (pointNormal.x < 0) *pageNumber = NO_PAGE;
    else *pageNumber = static_cast<size_t>(floor(pointNormal.x));
}

void list_page_layout::calculateBoundsNormal(m::rectangle* bounds)
{
    size_t pageCount = _notebook.lock()->model()->pageCount();

    bounds->left = 0.f;
    bounds->top = 0.f;
    bounds->right = _stacked ? 1.f : pageCount;
    bounds->bottom = (pageCount > 0) ? 1.f : 0.f;
}

void list_page_layout::calculateCameraBoundsNormal(
    const m::vector2d& offsetXY,
    const m::vector2d& focus,
    m::rectangle* bounds
    )
{
    m::point3d cameraWorld(
        offsetXY.x,
        offsetXY.y,
        0.f
        );

    m::point3d cameraNormal;
    this->worldToNormal(cameraWorld, &cameraNormal);

    m::point2d focusNormal(
        _focus.x / this->getSlotWidth(),
        _focus.y / constants::general::pageWorldDimensions().y
        );

    // camera: top-left, bottom-right
    bounds->left = cameraNormal.x - (focusNormal.x / 2.f);
    bounds->top = cameraNormal.y - (focusNormal.y / 2.f);
    bounds->right = cameraNormal.x + (focusNormal.x / 2.f);
    bounds->bottom = cameraNormal.y + (focusNormal.y / 2.f) ;
}

void list_page_layout::calculateScroll(
    const m::vector2d& offsetXY,
    const m::vector2d& focus,
    m::point2d& dimensions,
    m::point2d& position
    )
{
    m::rectangle cameraBoundsNormal;
    this->calculateCameraBoundsNormal(offsetXY, focus, &cameraBoundsNormal);

    m::rectangle boundsNormal;
    this->calculateBoundsNormal(&boundsNormal);

    b::float32 totalWidth = boundsNormal.right - boundsNormal.left;
    b::float32 totalHeight = boundsNormal.bottom - boundsNormal.top;

    dimensions.x = (cameraBoundsNormal.right - cameraBoundsNormal.left) / totalWidth;
    dimensions.y = (cameraBoundsNormal.bottom - cameraBoundsNormal.top) / totalHeight;

    position.x = (cameraBoundsNormal.left) / totalWidth;
    position.y = (cameraBoundsNormal.top) / totalHeight;
}

b::float32 list_page_layout::getSlotWidth()
{
    if (_stacked)
    {
        return constants::general::pageWorldDimensions().x;
    }
    else
    {
        return constants::general::pageSlotWorldDimensions().x;
    }
}
}} // namespace journal::view